package eval.deploy;

import java.util.ArrayList;
import cern.jet.random.AbstractDistribution;
import cern.jet.random.Normal;
import cern.jet.random.Poisson;
import com.google.inject.Inject;
import eval.exceptions.NotImplementedExecption;
import eval.utilities.configuration.ExperimentConfiguration;
import eval.utilities.random.ProbabilisticDistributionsEnum;
import eval.utilities.random.RandomUtilities;
import eval.vm.IVMFactory;

/**
 * Initializes the Deploy Manager
 * 
 * @author Amit Peled   <amit.kapina@gmail.com>
 * @author Igal Kaufman <igal.kaufman@gmail.com>
 *
 */
public class DeployGenerator implements IDeployGenerator {

	private static ExperimentConfiguration configuration = ExperimentConfiguration
			.getSingleton();

	private IDeployManager deployManager;
	private IVMFactory vmFactory;
	private AbstractDistribution newDeployDistribution = null;
	private IDemandManager demandManager;
	private int  negativeNumberOfRequestedVMs = 0;
	private RandomUtilities randTmp;

	/**
	 * @Constructor 
	 * @param deployManager
	 * @param vmFactory
	 * @param demandManager
	 */
	@Inject
	public DeployGenerator(IDeployManager deployManager, IVMFactory vmFactory, IDemandManager demandManager) {
		this.deployManager = deployManager;
		this.vmFactory = vmFactory;
		this.demandManager = demandManager;
		randTmp = new RandomUtilities();

	}

	/* (non-Javadoc)
	 * @see eval.deploy.IDeployGenerator#generateDeploys()
	 */
	@Override
	public void generateDeploys() {
		//we assume that we start the experiment with VM to deploy
		generateInitialCycleDeploy();
		generateOtherCyclesDeploys();
	}


	/*
	 * generates the initial deploy set 
	 */
	private void generateInitialCycleDeploy() {
		DeploySet ds = new DeploySet(vmFactory,extractDeployLifeTime(configuration.numberOfInitialVMs));
		deployManager.setDeploySetAtTick(ds, 0);
		demandManager.genrateDemand(ds.getVMs(),0);
	}

	/*
	 *generates deploy set except the first cycle  
	 */
	private void generateOtherCyclesDeploys() {

		int nextDeployTick = extractNewDeployCycle();

		for (int tick = 1; tick <configuration.experimentTicks; tick++) {
			DeploySet ds = new DeploySet();
			if (nextDeployTick == tick) {
				ds = createNewDeploySet();
				nextDeployTick = tick + Math.max(1,extractNewDeployCycle());
			}
			deployManager.setDeploySetAtTick(ds, tick);
			demandManager.genrateDemand(ds.getVMs(),tick);
		}
	}

	/*
	 * @return DeploySet
	 */
	private DeploySet createNewDeploySet() {
		int extractNumberOfVMsToDeploy = extractNumberOfVMsToDeploy();
		ArrayList<Integer> deployLifetime = extractDeployLifeTime(extractNumberOfVMsToDeploy);
		return new DeploySet(vmFactory, deployLifetime);
	}

	/*
	 * 
	 * @return the life time of VM
	 */
	private int extractNewDeployCycle() {
		switch (ProbabilisticDistributionsEnum
				.valueOf(configuration.averageTimeBetweenDeployRequestsDistribution)) {

				case NORMAL: 
					if (this.newDeployDistribution == null) {
						newDeployDistribution = randTmp
								.getNewNormalDistribution(configuration.averageTimeBetweenDeployRequests,configuration.averageTimeBetweenDeployRequests*0.2);
					}
					return newDeployDistribution.nextInt();

				case POISSON:
					if (this.newDeployDistribution == null) {
						newDeployDistribution = randTmp
								.getNewPoissonDistribution(configuration.averageTimeBetweenDeployRequests);
					}
					return newDeployDistribution.nextInt();

				case CONSTANT:
					return configuration.averageTimeBetweenDeployRequests;

				default:
					throw new IllegalArgumentException(
							"The requested distribution for deploy is not implemented");
		}
	}

	/*
	 * @param numberOfVMsToDeploy
	 * @return the life time of VMs 
	 */
	private ArrayList<Integer> extractDeployLifeTime(int numberOfVMsToDeploy) {

		ArrayList<Integer> deployLifeTimeList = new ArrayList<Integer>();
		int average = configuration.deploySetAverageDuration;
		switch (ProbabilisticDistributionsEnum
				.valueOf(configuration.deploySetsDurationDistribution)) {
				case POISSON:
					for (int i=0;i<numberOfVMsToDeploy;i++) {
						Poisson distribution = randTmp
								.getNewPoissonDistribution(average);
						deployLifeTimeList.add(distribution.nextInt() + 1);
					}
					return deployLifeTimeList;
				case CONSTANT:
					for (int i=0;i<numberOfVMsToDeploy;i++) {
						deployLifeTimeList.add(average);
					}
					return deployLifeTimeList;
				case NORMAL:
					for (int i=0;i<numberOfVMsToDeploy;i++) {
						Normal distribution = randTmp.getNewNormalDistribution(average, 
								average * configuration.deploySetAverageDurationSTD);
						deployLifeTimeList.add( new Integer(distribution.nextInt() + 1));

					}
					return deployLifeTimeList;
				default:
					throw new NotImplementedExecption(
							configuration.deploySetsDurationDistribution
							+ " is not implemented in the set of know distributions");
		}
	}

	/*
	 * @return number of VMs to deploy
	 */
	private int extractNumberOfVMsToDeploy() {
		switch (ProbabilisticDistributionsEnum
				.valueOf(configuration.averageNumberOfRequestedVMsDistribution)) {
				case NORMAL:
					Normal tmp = randTmp.
					getNewNormalDistribution(configuration.averageNumberOfRequestedVMs, configuration.averageNumberOfRequestedVMs * configuration.averageNumberOfRequestedVMsSTD);
					return makeSureReturnValueNotNegative(tmp.nextInt());
				case POISSON:
					Poisson distribution = randTmp
					.getNewPoissonDistribution(configuration.averageNumberOfRequestedVMs);
					return  distribution.nextInt();
				case CONSTANT:
					return  configuration.averageNumberOfRequestedVMs;

				default:
					throw new IllegalArgumentException(
							"The requested distribution for deploy is not implemented");
		}

	}

	/*
	 * if we we get a negative number, add it to the next tick
	 * @param averageNumberOfRequestedVMs
	 * @return the life time
	 */
	private int makeSureReturnValueNotNegative(int averageNumberOfRequestedVMs){
		if(averageNumberOfRequestedVMs < 0){
			this.negativeNumberOfRequestedVMs += averageNumberOfRequestedVMs;
			return 0;
		} else {
			averageNumberOfRequestedVMs += this.negativeNumberOfRequestedVMs;
			if(averageNumberOfRequestedVMs < 0){
				this.negativeNumberOfRequestedVMs = averageNumberOfRequestedVMs;
				return 0;
			} else{
				this.negativeNumberOfRequestedVMs = 0;
				return averageNumberOfRequestedVMs;
			}
		}
	}
}

